/*
tidy.c - HTML TidyLib command line driver

Copyright (c) 1998-2008 World Wide Web Consortium
(Massachusetts Institute of Technology, European Research
Consortium for Informatics and Mathematics, Keio University).
All Rights Reserved.

*/

#include "tidy.h"
#if !defined(NDEBUG) && defined(_MSC_VER)
#include "sprtf.h"
#endif

#ifndef SPRTF
#define SPRTF printf
#endif

static FILE* errout = NULL;  /* set to stderr */
/* static FILE* txtout = NULL; */  /* set to stdout */

static Bool samefile(ctmbstr filename1, ctmbstr filename2)
{
#if FILENAMES_CASE_SENSITIVE
	return (strcmp(filename1, filename2) == 0);
#else
	return (strcasecmp(filename1, filename2) == 0);
#endif
}

static void outOfMemory(void)
{
	fprintf(stderr, "Out of memory. Bailing out.");
	exit(1);
}

static const char *cutToWhiteSpace(const char *s, uint offset, char *sbuf)
{
	if (!s)
	{
		sbuf[0] = '\0';
		return NULL;
	}
	else if (strlen(s) <= offset)
	{
		strcpy(sbuf, s);
		sbuf[offset] = '\0';
		return NULL;
	}
	else
	{
		uint j, l, n;
		j = offset;
		while (j && s[j] != ' ')
			--j;
		l = j;
		n = j + 1;
		/* no white space */
		if (j == 0)
		{
			l = offset;
			n = offset;
		}
		strncpy(sbuf, s, l);
		sbuf[l] = '\0';
		return s + n;
	}
}

static void print2Columns(const char* fmt, uint l1, uint l2,
	const char *c1, const char *c2)
{
	const char *pc1 = c1, *pc2 = c2;
	char *c1buf = (char *)malloc(l1 + 1);
	char *c2buf = (char *)malloc(l2 + 1);
	if (!c1buf) outOfMemory();
	if (!c2buf) outOfMemory();

	do
	{
		pc1 = cutToWhiteSpace(pc1, l1, c1buf);
		pc2 = cutToWhiteSpace(pc2, l2, c2buf);
		printf(fmt,
			c1buf[0] != '\0' ? c1buf : "",
			c2buf[0] != '\0' ? c2buf : "");
	} while (pc1 || pc2);
	free(c1buf);
	free(c2buf);
}

static void print3Columns(const char* fmt, uint l1, uint l2, uint l3,
	const char *c1, const char *c2, const char *c3)
{
	const char *pc1 = c1, *pc2 = c2, *pc3 = c3;
	char *c1buf = (char *)malloc(l1 + 1);
	char *c2buf = (char *)malloc(l2 + 1);
	char *c3buf = (char *)malloc(l3 + 1);
	if (!c1buf) outOfMemory();
	if (!c2buf) outOfMemory();
	if (!c3buf) outOfMemory();

	do
	{
		pc1 = cutToWhiteSpace(pc1, l1, c1buf);
		pc2 = cutToWhiteSpace(pc2, l2, c2buf);
		pc3 = cutToWhiteSpace(pc3, l3, c3buf);
		printf(fmt,
			c1buf[0] != '\0' ? c1buf : "",
			c2buf[0] != '\0' ? c2buf : "",
			c3buf[0] != '\0' ? c3buf : "");
	} while (pc1 || pc2 || pc3);
	free(c1buf);
	free(c2buf);
	free(c3buf);
}

static const char helpfmt[] = " %-19.19s %-58.58s\n";
static const char helpul[]
= "-----------------------------------------------------------------";
static const char fmt[] = "%-27.27s %-9.9s  %-40.40s\n";
static const char valfmt[] = "%-27.27s %-9.9s %-1.1s%-39.39s\n";
static const char ul[]
= "=================================================================";

typedef enum
{
	CmdOptFileManip,
	CmdOptCatFIRST = CmdOptFileManip,
	CmdOptProcDir,
	CmdOptCharEnc,
	CmdOptMisc,
	CmdOptCatLAST
} CmdOptCategory;

static const struct {
	ctmbstr mnemonic;
	ctmbstr name;
} cmdopt_catname[] = {
	{ "file-manip", "File manipulation" },
	{ "process-directives", "Processing directives" },
	{ "char-encoding", "Character encodings" },
	{ "misc", "Miscellaneous" }
};

typedef struct {
	ctmbstr name1;      /**< Name */
	ctmbstr desc;       /**< Description */
	ctmbstr eqconfig;   /**< Equivalent configuration option */
	CmdOptCategory cat; /**< Category */
	ctmbstr name2;      /**< Name */
	ctmbstr name3;      /**< Name */
} CmdOptDesc;

static const CmdOptDesc cmdopt_defs[] = {
	{ "-output <file>",
	"write output to the specified <file>",
	"output-file: <file>", CmdOptFileManip, "-o <file>" },
	{ "-config <file>",
	"set configuration options from the specified <file>",
	NULL, CmdOptFileManip },
	{ "-file <file>",
	"write errors and warnings to the specified <file>",
	"error-file: <file>", CmdOptFileManip, "-f <file>" },
	{ "-modify",
	"modify the original input files",
	"write-back: yes", CmdOptFileManip, "-m" },
	{ "-indent",
	"indent element content",
	"indent: auto", CmdOptProcDir, "-i" },
	{ "-wrap <column>",
	"wrap text at the specified <column>"
	". 0 is assumed if <column> is missing. "
	"When this option is omitted, the default of the configuration option "
	"\"wrap\" applies.",
	"wrap: <column>", CmdOptProcDir, "-w <column>" },
	{ "-upper",
	"force tags to upper case",
	"uppercase-tags: yes", CmdOptProcDir, "-u" },
	{ "-clean",
	"replace FONT, NOBR and CENTER tags by CSS",
	"clean: yes", CmdOptProcDir, "-c" },
	{ "-bare",
	"strip out smart quotes and em dashes, etc.",
	"bare: yes", CmdOptProcDir, "-b" },
	{ "-gdoc",
	"produce clean version of html exported by google docs",
	"gdoc: yes", CmdOptProcDir, "-g" },
	{ "-numeric",
	"output numeric rather than named entities",
	"numeric-entities: yes", CmdOptProcDir, "-n" },
	{ "-errors",
	"show only errors and warnings",
	"markup: no", CmdOptProcDir, "-e" },
	{ "-quiet",
	"suppress nonessential output",
	"quiet: yes", CmdOptProcDir, "-q" },
	{ "-omit",
	"omit optional start tags and end tags",
	"omit-optional-tags: yes", CmdOptProcDir },
	{ "-xml",
	"specify the input is well formed XML",
	"input-xml: yes", CmdOptProcDir },
	{ "-asxml",
	"convert HTML to well formed XHTML",
	"output-xhtml: yes", CmdOptProcDir, "-asxhtml" },
	{ "-ashtml",
	"force XHTML to well formed HTML",
	"output-html: yes", CmdOptProcDir },
#if SUPPORT_ACCESSIBILITY_CHECKS
	{ "-access <level>",
	"do additional accessibility checks (<level> = 0, 1, 2, 3)"
	". 0 is assumed if <level> is missing.",
	"accessibility-check: <level>", CmdOptProcDir },
#endif
	{ "-raw",
	"output values above 127 without conversion to entities",
	NULL, CmdOptCharEnc },
	{ "-ascii",
	"use ISO-8859-1 for input, US-ASCII for output",
	NULL, CmdOptCharEnc },
	{ "-latin0",
	"use ISO-8859-15 for input, US-ASCII for output",
	NULL, CmdOptCharEnc },
	{ "-latin1",
	"use ISO-8859-1 for both input and output",
	NULL, CmdOptCharEnc },
#ifndef NO_NATIVE_ISO2022_SUPPORT
	{ "-iso2022",
	"use ISO-2022 for both input and output",
	NULL, CmdOptCharEnc },
#endif
	{ "-utf8",
	"use UTF-8 for both input and output",
	NULL, CmdOptCharEnc },
	{ "-mac",
	"use MacRoman for input, US-ASCII for output",
	NULL, CmdOptCharEnc },
	{ "-win1252",
	"use Windows-1252 for input, US-ASCII for output",
	NULL, CmdOptCharEnc },
	{ "-ibm858",
	"use IBM-858 (CP850+Euro) for input, US-ASCII for output",
	NULL, CmdOptCharEnc },
#if SUPPORT_UTF16_ENCODINGS
	{ "-utf16le",
	"use UTF-16LE for both input and output",
	NULL, CmdOptCharEnc },
	{ "-utf16be",
	"use UTF-16BE for both input and output",
	NULL, CmdOptCharEnc },
	{ "-utf16",
	"use UTF-16 for both input and output",
	NULL, CmdOptCharEnc },
#endif
#if SUPPORT_ASIAN_ENCODINGS /* #431953 - RJ */
	{ "-big5",
	"use Big5 for both input and output",
	NULL, CmdOptCharEnc },
	{ "-shiftjis",
	"use Shift_JIS for both input and output",
	NULL, CmdOptCharEnc },
	{ "-language <lang>",
	"set the two-letter language code <lang> (for future use)",
	"language: <lang>", CmdOptCharEnc },
#endif
	{ "-version",
	"show the version of Tidy",
	NULL, CmdOptMisc, "-v" },
	{ "-help",
	"list the command line options",
	NULL, CmdOptMisc, "-h", "-?" },
	{ "-xml-help",
	"list the command line options in XML format",
	NULL, CmdOptMisc },
	{ "-help-config",
	"list all configuration options",
	NULL, CmdOptMisc },
	{ "-xml-config",
	"list all configuration options in XML format",
	NULL, CmdOptMisc },
	{ "-show-config",
	"list the current configuration settings",
	NULL, CmdOptMisc },
	{ NULL, NULL, NULL, CmdOptMisc }
};

static tmbstr get_option_names(const CmdOptDesc* pos)
{
	tmbstr name;
	uint len = strlen(pos->name1);
	if (pos->name2)
		len += 2 + strlen(pos->name2);
	if (pos->name3)
		len += 2 + strlen(pos->name3);

	name = (tmbstr)malloc(len + 1);
	if (!name) outOfMemory();
	strcpy(name, pos->name1);
	if (pos->name2)
	{
		strcat(name, ", ");
		strcat(name, pos->name2);
	}
	if (pos->name3)
	{
		strcat(name, ", ");
		strcat(name, pos->name3);
	}
	return name;
}

static tmbstr get_escaped_name(ctmbstr name)
{
	tmbstr escpName;
	char aux[2];
	uint len = 0;
	ctmbstr c;
	for (c = name; *c != '\0'; ++c)
		switch (*c)
	{
		case '<':
		case '>':
			len += 4;
			break;
		case '"':
			len += 6;
			break;
		default:
			len += 1;
			break;
	}

	escpName = (tmbstr)malloc(len + 1);
	if (!escpName) outOfMemory();
	escpName[0] = '\0';

	aux[1] = '\0';
	for (c = name; *c != '\0'; ++c)
		switch (*c)
	{
		case '<':
			strcat(escpName, "&lt;");
			break;
		case '>':
			strcat(escpName, "&gt;");
			break;
		case '"':
			strcat(escpName, "&quot;");
			break;
		default:
			aux[0] = *c;
			strcat(escpName, aux);
			break;
	}

	return escpName;
}

static void print_help_option(void)
{
	CmdOptCategory cat = CmdOptCatFIRST;
	const CmdOptDesc* pos = cmdopt_defs;

	for (cat = CmdOptCatFIRST; cat != CmdOptCatLAST; ++cat)
	{
		size_t len = strlen(cmdopt_catname[cat].name);
		printf("%s\n", cmdopt_catname[cat].name);
		printf("%*.*s\n", (int)len, (int)len, helpul);
		for (pos = cmdopt_defs; pos->name1; ++pos)
		{
			tmbstr name;
			if (pos->cat != cat)
				continue;
			name = get_option_names(pos);
			print2Columns(helpfmt, 19, 58, name, pos->desc);
			free(name);
		}
		printf("\n");
	}
}

static void print_xml_help_option_element(ctmbstr element, ctmbstr name)
{
	tmbstr escpName;
	if (!name)
		return;
	printf("  <%s>%s</%s>\n", element, escpName = get_escaped_name(name),
		element);
	free(escpName);
}

static void print_xml_help_option(void)
{
	const CmdOptDesc* pos = cmdopt_defs;

	for (pos = cmdopt_defs; pos->name1; ++pos)
	{
		printf(" <option class=\"%s\">\n", cmdopt_catname[pos->cat].mnemonic);
		print_xml_help_option_element("name", pos->name1);
		print_xml_help_option_element("name", pos->name2);
		print_xml_help_option_element("name", pos->name3);
		print_xml_help_option_element("description", pos->desc);
		if (pos->eqconfig)
			print_xml_help_option_element("eqconfig", pos->eqconfig);
		else
			printf("  <eqconfig />\n");
		printf(" </option>\n");
	}
}

static void xml_help(void)
{
	printf("<?xml version=\"1.0\"?>\n"
		"<cmdline version=\"%s\">\n", tidyLibraryVersion());
	print_xml_help_option();
	printf("</cmdline>\n");
}

static ctmbstr get_final_name(ctmbstr prog)
{
	ctmbstr name = prog;
	int c;
	size_t i, len = strlen(prog);
	for (i = 0; i < len; i++) {
		c = prog[i];
		if (((c == '/') || (c == '\\')) && prog[i + 1])
			name = &prog[i + 1];
	}
	return name;
}

static void help(ctmbstr prog)
{
	printf("\n");
	printf("%s [options...] [file...] [options...] [file...]\n", get_final_name(prog));
	printf("Utility to clean up and pretty print HTML/XHTML/XML.\n");
	printf("\n");

	printf("This is modern HTML Tidy version %s.\n", tidyLibraryVersion());
	printf("\n");

#ifdef PLATFORM_NAME
	printf("Options for HTML Tidy for %s:\n", PLATFORM_NAME);
#else
	printf("Options for HTML Tidy:\n");
#endif
	printf("\n");

	print_help_option();

	printf("Use --optionX valueX for any configuration option \"optionX\" with argument\n"
		"\"valueX\". For a list of the configuration options, use \"-help-config\" or refer\n"
		"to the man page.\n\n");

	printf("Input/Output default to stdin/stdout respectively.\n");
	printf("\n");
	printf("Single letter options apart from -f may be combined\n");
	printf("as in:  tidy -f errs.txt -imu foo.html\n");
	printf("\n");
	printf("For more information about HTML Tidy, see\n");
	printf("  http://www.html-tidy.org/\n");
	printf("\n");
	printf("For more information on HTML, see the following:\n");
	printf("\n");
	printf("  HTML: Edition for Web Authors (the latest HTML specification)\n");
	printf("  http://dev.w3.org/html5/spec-author-view\n");
	printf("\n");
	printf("  HTML: The Markup Language (an HTML language reference)\n");
	printf("  http://dev.w3.org/html5/markup/\n");
	printf("\n");
	printf("File bug reports at https://github.com/htacg/tidy-html5/issues/\n");
	printf("or send questions and comments to public-htacg@w3.org.\n");
	printf("\n");
	printf("Validate your HTML documents using the W3C Nu Markup Validator:\n");
	printf("\n");
	printf("  http://validator.w3.org/nu/");
	printf("\n\n");
}

static Bool isAutoBool(TidyOption topt)
{
	TidyIterator pos;
	ctmbstr def;

	if (tidyOptGetType(topt) != TidyInteger)
		return no;

	pos = tidyOptGetPickList(topt);
	while (pos)
	{
		def = tidyOptGetNextPick(topt, &pos);
		if (0 == strcmp(def, "yes"))
			return yes;
	}
	return no;
}

static
ctmbstr ConfigCategoryName(TidyConfigCategory id)
{
	switch (id)
	{
	case TidyMarkup:
		return "markup";
	case TidyDiagnostics:
		return "diagnostics";
	case TidyPrettyPrint:
		return "print";
	case TidyEncoding:
		return "encoding";
	case TidyMiscellaneous:
		return "misc";
	}
	fprintf(stderr, "Fatal error: impossible value for id='%d'.\n", (int)id);
	assert(0);
	abort();
	return "never_here"; /* only for the compiler warning */
}

/* Description of an option */
typedef struct {
	ctmbstr name;  /**< Name */
	ctmbstr cat;   /**< Category */
	ctmbstr type;  /**< "String, ... */
	ctmbstr vals;  /**< Potential values. If NULL, use an external function */
	ctmbstr def;   /**< default */
	tmbchar tempdefs[80]; /**< storage for default such as integer */
	Bool haveVals; /**< if yes, vals is valid */
} OptionDesc;

typedef void(*OptionFunc)(TidyDoc, TidyOption, OptionDesc *);


/* Create description "d" related to "opt" */
static
void GetOption(TidyDoc tdoc, TidyOption topt, OptionDesc *d)
{
	TidyOptionId optId = tidyOptGetId(topt);
	TidyOptionType optTyp = tidyOptGetType(topt);

	d->name = tidyOptGetName(topt);
	d->cat = ConfigCategoryName(tidyOptGetCategory(topt));
	d->vals = NULL;
	d->def = NULL;
	d->haveVals = yes;

	/* Handle special cases first.
	*/
	switch (optId)
	{
	case TidyDuplicateAttrs:
	case TidySortAttributes:
	case TidyNewline:
	case TidyAccessibilityCheckLevel:
		d->type = "enum";
		d->vals = NULL;
		d->def =
			optId == TidyNewline ?
			"<em>Platform dependent</em>"
			: tidyOptGetCurrPick(tdoc, optId);
		break;

	case TidyDoctype:
		d->type = "DocType";
		d->vals = NULL;
		{
			ctmbstr sdef = NULL;
			sdef = tidyOptGetCurrPick(tdoc, TidyDoctypeMode);
			if (!sdef || *sdef == '*')
				sdef = tidyOptGetValue(tdoc, TidyDoctype);
			d->def = sdef;
		}
		break;

	case TidyInlineTags:
	case TidyBlockTags:
	case TidyEmptyTags:
	case TidyPreTags:
		d->type = "Tag names";
		d->vals = "tagX, tagY, ...";
		d->def = NULL;
		break;

	case TidyCharEncoding:
	case TidyInCharEncoding:
	case TidyOutCharEncoding:
		d->type = "Encoding";
		d->def = tidyOptGetEncName(tdoc, optId);
		if (!d->def)
			d->def = "?";
		d->vals = NULL;
		break;

		/* General case will handle remaining */
	default:
		switch (optTyp)
		{
		case TidyBoolean:
			d->type = "Boolean";
			d->vals = "y/n, yes/no, t/f, true/false, 1/0";
			d->def = tidyOptGetCurrPick(tdoc, optId);
			break;

		case TidyInteger:
			if (isAutoBool(topt))
			{
				d->type = "AutoBool";
				d->vals = "auto, y/n, yes/no, t/f, true/false, 1/0";
				d->def = tidyOptGetCurrPick(tdoc, optId);
			}
			else
			{
				uint idef;
				d->type = "Integer";
				if (optId == TidyWrapLen)
					d->vals = "0 (no wrapping), 1, 2, ...";
				else
					d->vals = "0, 1, 2, ...";

				idef = tidyOptGetInt(tdoc, optId);
				sprintf(d->tempdefs, "%u", idef);
				d->def = d->tempdefs;
			}
			break;

		case TidyString:
			d->type = "String";
			d->vals = NULL;
			d->haveVals = no;
			d->def = tidyOptGetValue(tdoc, optId);
			break;
		}
	}
}

/* Array holding all options. Contains a trailing sentinel. */
typedef struct {
	TidyOption topt[N_TIDY_OPTIONS];
} AllOption_t;

static
int cmpOpt(const void* e1_, const void *e2_)
{
	const TidyOption* e1 = (const TidyOption*)e1_;
	const TidyOption* e2 = (const TidyOption*)e2_;
	return strcmp(tidyOptGetName(*e1), tidyOptGetName(*e2));
}

static
void getSortedOption(TidyDoc tdoc, AllOption_t *tOption)
{
	TidyIterator pos = tidyGetOptionList(tdoc);
	uint i = 0;

	while (pos)
	{
		TidyOption topt = tidyGetNextOption(tdoc, &pos);
		tOption->topt[i] = topt;
		++i;
	}
	tOption->topt[i] = NULL; /* sentinel */

	qsort(tOption->topt,
		/* Do not sort the sentinel: hence `-1' */
		sizeof(tOption->topt) / sizeof(tOption->topt[0]) - 1,
		sizeof(tOption->topt[0]),
		cmpOpt);
}

static void ForEachSortedOption(TidyDoc tdoc, OptionFunc OptionPrint)
{
	AllOption_t tOption;
	const TidyOption *topt;

	getSortedOption(tdoc, &tOption);
	for (topt = tOption.topt; *topt; ++topt)
	{
		OptionDesc d;

		GetOption(tdoc, *topt, &d);
		(*OptionPrint)(tdoc, *topt, &d);
	}
}

static void ForEachOption(TidyDoc tdoc, OptionFunc OptionPrint)
{
	TidyIterator pos = tidyGetOptionList(tdoc);

	while (pos)
	{
		TidyOption topt = tidyGetNextOption(tdoc, &pos);
		OptionDesc d;

		GetOption(tdoc, topt, &d);
		(*OptionPrint)(tdoc, topt, &d);
	}
}

static
void PrintAllowedValuesFromPick(TidyOption topt)
{
	TidyIterator pos = tidyOptGetPickList(topt);
	Bool first = yes;
	ctmbstr def;
	while (pos)
	{
		if (first)
			first = no;
		else
			printf(", ");
		def = tidyOptGetNextPick(topt, &pos);
		printf("%s", def);
	}
}

static
void PrintAllowedValues(TidyOption topt, const OptionDesc *d)
{
	if (d->vals)
		printf("%s", d->vals);
	else
		PrintAllowedValuesFromPick(topt);
}

static
void printXMLDescription(TidyDoc tdoc, TidyOption topt)
{
	ctmbstr doc = tidyOptGetDoc(tdoc, topt);

	if (doc)
		printf("  <description>%s</description>\n", doc);
	else
	{
		printf("  <description />\n");
		fprintf(stderr, "Warning: option `%s' is not documented.\n",
			tidyOptGetName(topt));
	}
}

static
void printXMLCrossRef(TidyDoc tdoc, TidyOption topt)
{
	TidyOption optLinked;
	TidyIterator pos = tidyOptGetDocLinksList(tdoc, topt);
	while (pos)
	{
		optLinked = tidyOptGetNextDocLinks(tdoc, &pos);
		printf("  <seealso>%s</seealso>\n", tidyOptGetName(optLinked));
	}
}

static
void printXMLOption(TidyDoc tdoc, TidyOption topt, OptionDesc *d)
{
	if (tidyOptIsReadOnly(topt))
		return;

	printf(" <option class=\"%s\">\n", d->cat);
	printf("  <name>%s</name>\n", d->name);
	printf("  <type>%s</type>\n", d->type);
	if (d->def)
		printf("  <default>%s</default>\n", d->def);
	else
		printf("  <default />\n");
	if (d->haveVals)
	{
		printf("  <example>");
		PrintAllowedValues(topt, d);
		printf("</example>\n");
	}
	else
	{
		printf("  <example />\n");
	}
	printXMLDescription(tdoc, topt);
	printXMLCrossRef(tdoc, topt);
	printf(" </option>\n");
}

static void XMLoptionhelp(TidyDoc tdoc)
{
	printf("<?xml version=\"1.0\"?>\n"
		"<config version=\"%s\">\n", tidyLibraryVersion());
	ForEachOption(tdoc, printXMLOption);
	printf("</config>\n");
}

static
tmbstr GetAllowedValuesFromPick(TidyOption topt)
{
	TidyIterator pos;
	Bool first;
	ctmbstr def;
	uint len = 0;
	tmbstr val;

	pos = tidyOptGetPickList(topt);
	first = yes;
	while (pos)
	{
		if (first)
			first = no;
		else
			len += 2;
		def = tidyOptGetNextPick(topt, &pos);
		len += strlen(def);
	}
	val = (tmbstr)malloc(len + 1);
	if (!val) outOfMemory();
	val[0] = '\0';
	pos = tidyOptGetPickList(topt);
	first = yes;
	while (pos)
	{
		if (first)
			first = no;
		else
			strcat(val, ", ");
		def = tidyOptGetNextPick(topt, &pos);
		strcat(val, def);
	}
	return val;
}

static
tmbstr GetAllowedValues(TidyOption topt, const OptionDesc *d)
{
	if (d->vals)
	{
		tmbstr val = (tmbstr)malloc(1 + strlen(d->vals));
		if (!val) outOfMemory();
		strcpy(val, d->vals);
		return val;
	}
	else
		return GetAllowedValuesFromPick(topt);
}

static
void printOption(TidyDoc ARG_UNUSED(tdoc), TidyOption topt,
OptionDesc *d)
{
	if (tidyOptIsReadOnly(topt))
		return;

	if (*d->name || *d->type)
	{
		ctmbstr pval = d->vals;
		tmbstr val = NULL;
		if (!d->haveVals)
		{
			pval = "-";
		}
		else if (pval == NULL)
		{
			val = GetAllowedValues(topt, d);
			pval = val;
		}
		print3Columns(fmt, 27, 9, 40, d->name, d->type, pval);
		if (val)
			free(val);
	}
}

static void optionhelp(TidyDoc tdoc)
{
	printf("\nHTML Tidy Configuration Settings\n\n");
	printf("Within a file, use the form:\n\n");
	printf("wrap: 72\n");
	printf("indent: no\n\n");
	printf("When specified on the command line, use the form:\n\n");
	printf("--wrap 72 --indent no\n\n");

	printf(fmt, "Name", "Type", "Allowable values");
	printf(fmt, ul, ul, ul);

	ForEachSortedOption(tdoc, printOption);
}

static
void printOptionValues(TidyDoc ARG_UNUSED(tdoc), TidyOption topt,
OptionDesc *d)
{
	TidyOptionId optId = tidyOptGetId(topt);
	ctmbstr ro = tidyOptIsReadOnly(topt) ? "*" : "";

	switch (optId)
	{
	case TidyInlineTags:
	case TidyBlockTags:
	case TidyEmptyTags:
	case TidyPreTags:
	{
						TidyIterator pos = tidyOptGetDeclTagList(tdoc);
						while (pos)
						{
							d->def = tidyOptGetNextDeclTag(tdoc, optId, &pos);
							if (pos)
							{
								if (*d->name)
									printf(valfmt, d->name, d->type, ro, d->def);
								else
									printf(fmt, d->name, d->type, d->def);
								d->name = "";
								d->type = "";
							}
						}
	}
		break;
	case TidyNewline:
		d->def = tidyOptGetCurrPick(tdoc, optId);
		break;
	default:
		break;
	}

	/* fix for http://tidy.sf.net/bug/873921 */
	if (*d->name || *d->type || (d->def && *d->def))
	{
		if (!d->def)
			d->def = "";
		if (*d->name)
			printf(valfmt, d->name, d->type, ro, d->def);
		else
			printf(fmt, d->name, d->type, d->def);
	}
}

static void optionvalues(TidyDoc tdoc)
{
	printf("\nConfiguration File Settings:\n\n");
	printf(fmt, "Name", "Type", "Current Value");
	printf(fmt, ul, ul, ul);

	ForEachSortedOption(tdoc, printOptionValues);

	printf("\n\nValues marked with an *asterisk are calculated \n"
		"internally by HTML Tidy\n\n");
}

static void version(void)
{
#ifdef PLATFORM_NAME
	printf("HTML Tidy for %s version %s\n",
		PLATFORM_NAME, tidyLibraryVersion());
#else
	printf("HTML Tidy version %s\n", tidyLibraryVersion());
#endif
}

static void unknownOption(uint c)
{
	fprintf(errout, "HTML Tidy: unknown option: %c\n", (char)c);
}

int main(int argc, char** argv)
{
	ctmbstr prog = argv[0];
	ctmbstr cfgfil = NULL, errfil = NULL, htmlfil = NULL;
	TidyDoc tdoc = tidyCreate();
	int status = 0;

	uint contentErrors = 0;
	uint contentWarnings = 0;
	uint accessWarnings = 0;

	errout = stderr;  /* initialize to stderr */
	status = 0;
#if !defined(NDEBUG) && defined(_MSC_VER)
	set_log_file((char *)"temptidy.txt", 0);
	// add_append_log(1);
#endif

#ifdef TIDY_CONFIG_FILE
	if (tidyFileExists(tdoc, TIDY_CONFIG_FILE))
	{
		status = tidyLoadConfig(tdoc, TIDY_CONFIG_FILE);
		if (status != 0)
			fprintf(errout, "Loading config file \"%s\" failed, err = %d\n", TIDY_CONFIG_FILE, status);
	}
#endif /* TIDY_CONFIG_FILE */

	/* look for env var "HTML_TIDY" */
	/* then for ~/.tidyrc (on platforms defining $HOME) */

	if ((cfgfil = getenv("HTML_TIDY")) != NULL)
	{
		status = tidyLoadConfig(tdoc, cfgfil);
		if (status != 0)
			fprintf(errout, "Loading config file \"%s\" failed, err = %d\n", cfgfil, status);
	}
#ifdef TIDY_USER_CONFIG_FILE
	else if (tidyFileExists(tdoc, TIDY_USER_CONFIG_FILE))
	{
		status = tidyLoadConfig(tdoc, TIDY_USER_CONFIG_FILE);
		if (status != 0)
			fprintf(errout, "Loading config file \"%s\" failed, err = %d\n", TIDY_USER_CONFIG_FILE, status);
	}
#endif /* TIDY_USER_CONFIG_FILE */

	/* read command line */
	while (argc > 0)
	{
		if (argc > 1 && argv[1][0] == '-')
		{
			/* support -foo and --foo */
			ctmbstr arg = argv[1] + 1;

			if (strcasecmp(arg, "xml") == 0)
				tidyOptSetBool(tdoc, TidyXmlTags, yes);

			else if (strcasecmp(arg, "asxml") == 0 ||
				strcasecmp(arg, "asxhtml") == 0)
			{
				tidyOptSetBool(tdoc, TidyXhtmlOut, yes);
			}
			else if (strcasecmp(arg, "ashtml") == 0)
				tidyOptSetBool(tdoc, TidyHtmlOut, yes);

			else if (strcasecmp(arg, "indent") == 0)
			{
				tidyOptSetInt(tdoc, TidyIndentContent, TidyAutoState);
				if (tidyOptGetInt(tdoc, TidyIndentSpaces) == 0)
					tidyOptResetToDefault(tdoc, TidyIndentSpaces);
			}
			else if (strcasecmp(arg, "omit") == 0)
				tidyOptSetBool(tdoc, TidyOmitOptionalTags, yes);

			else if (strcasecmp(arg, "upper") == 0)
				tidyOptSetBool(tdoc, TidyUpperCaseTags, yes);

			else if (strcasecmp(arg, "clean") == 0)
				tidyOptSetBool(tdoc, TidyMakeClean, yes);

			else if (strcasecmp(arg, "gdoc") == 0)
				tidyOptSetBool(tdoc, TidyGDocClean, yes);

			else if (strcasecmp(arg, "bare") == 0)
				tidyOptSetBool(tdoc, TidyMakeBare, yes);

			else if (strcasecmp(arg, "raw") == 0 ||
				strcasecmp(arg, "ascii") == 0 ||
				strcasecmp(arg, "latin0") == 0 ||
				strcasecmp(arg, "latin1") == 0 ||
				strcasecmp(arg, "utf8") == 0 ||
#ifndef NO_NATIVE_ISO2022_SUPPORT
				strcasecmp(arg, "iso2022") == 0 ||
#endif
#if SUPPORT_UTF16_ENCODINGS
				strcasecmp(arg, "utf16le") == 0 ||
				strcasecmp(arg, "utf16be") == 0 ||
				strcasecmp(arg, "utf16") == 0 ||
#endif
#if SUPPORT_ASIAN_ENCODINGS
				strcasecmp(arg, "shiftjis") == 0 ||
				strcasecmp(arg, "big5") == 0 ||
#endif
				strcasecmp(arg, "mac") == 0 ||
				strcasecmp(arg, "win1252") == 0 ||
				strcasecmp(arg, "ibm858") == 0)
			{
				tidySetCharEncoding(tdoc, arg);
			}
			else if (strcasecmp(arg, "numeric") == 0)
				tidyOptSetBool(tdoc, TidyNumEntities, yes);

			else if (strcasecmp(arg, "modify") == 0 ||
				strcasecmp(arg, "change") == 0 ||  /* obsolete */
				strcasecmp(arg, "update") == 0)   /* obsolete */
			{
				tidyOptSetBool(tdoc, TidyWriteBack, yes);
			}
			else if (strcasecmp(arg, "errors") == 0)
				tidyOptSetBool(tdoc, TidyShowMarkup, no);

			else if (strcasecmp(arg, "quiet") == 0)
				tidyOptSetBool(tdoc, TidyQuiet, yes);

			else if (strcasecmp(arg, "help") == 0 ||
				strcasecmp(arg, "h") == 0 || *arg == '?')
			{
				help(prog);
				tidyRelease(tdoc);
				return 0; /* success */
			}
			else if (strcasecmp(arg, "xml-help") == 0)
			{
				xml_help();
				tidyRelease(tdoc);
				return 0; /* success */
			}
			else if (strcasecmp(arg, "help-config") == 0)
			{
				optionhelp(tdoc);
				tidyRelease(tdoc);
				return 0; /* success */
			}
			else if (strcasecmp(arg, "xml-config") == 0)
			{
				XMLoptionhelp(tdoc);
				tidyRelease(tdoc);
				return 0; /* success */
			}
			else if (strcasecmp(arg, "show-config") == 0)
			{
				optionvalues(tdoc);
				tidyRelease(tdoc);
				return 0; /* success */
			}
			else if (strcasecmp(arg, "config") == 0)
			{
				if (argc >= 3)
				{
					ctmbstr post;

					tidyLoadConfig(tdoc, argv[2]);

					/* Set new error output stream if setting changed */
					post = tidyOptGetValue(tdoc, TidyErrFile);
					if (post && (!errfil || !samefile(errfil, post)))
					{
						errfil = post;
						errout = tidySetErrorFile(tdoc, post);
					}

					--argc;
					++argv;
				}
			}

#if SUPPORT_ASIAN_ENCODINGS
			else if (strcasecmp(arg, "language") == 0 ||
				strcasecmp(arg, "lang") == 0)
			{
				if (argc >= 3)
				{
					tidyOptSetValue(tdoc, TidyLanguage, argv[2]);
					--argc;
					++argv;
				}
			}
#endif

			else if (strcasecmp(arg, "output") == 0 ||
				strcasecmp(arg, "-output-file") == 0 ||
				strcasecmp(arg, "o") == 0)
			{
				if (argc >= 3)
				{
					tidyOptSetValue(tdoc, TidyOutFile, argv[2]);
					--argc;
					++argv;
				}
			}
			else if (strcasecmp(arg, "file") == 0 ||
				strcasecmp(arg, "-file") == 0 ||
				strcasecmp(arg, "f") == 0)
			{
				if (argc >= 3)
				{
					errfil = argv[2];
					errout = tidySetErrorFile(tdoc, errfil);
					--argc;
					++argv;
				}
			}
			else if (strcasecmp(arg, "wrap") == 0 ||
				strcasecmp(arg, "-wrap") == 0 ||
				strcasecmp(arg, "w") == 0)
			{
				if (argc >= 3)
				{
					uint wraplen = 0;
					int nfields = sscanf(argv[2], "%u", &wraplen);
					tidyOptSetInt(tdoc, TidyWrapLen, wraplen);
					if (nfields > 0)
					{
						--argc;
						++argv;
					}
				}
			}
			else if (strcasecmp(arg, "version") == 0 ||
				strcasecmp(arg, "-version") == 0 ||
				strcasecmp(arg, "v") == 0)
			{
				version();
				tidyRelease(tdoc);
				return 0;  /* success */

			}
			else if (strncmp(argv[1], "--", 2) == 0)
			{
				if (tidyOptParseValue(tdoc, argv[1] + 2, argv[2]))
				{
					/* Set new error output stream if setting changed */
					ctmbstr post = tidyOptGetValue(tdoc, TidyErrFile);
					if (post && (!errfil || !samefile(errfil, post)))
					{
						errfil = post;
						errout = tidySetErrorFile(tdoc, post);
					}

					++argv;
					--argc;
				}
			}

#if SUPPORT_ACCESSIBILITY_CHECKS
			else if (strcasecmp(arg, "access") == 0)
			{
				if (argc >= 3)
				{
					uint acclvl = 0;
					int nfields = sscanf(argv[2], "%u", &acclvl);
					tidyOptSetInt(tdoc, TidyAccessibilityCheckLevel, acclvl);
					if (nfields > 0)
					{
						--argc;
						++argv;
					}
				}
			}
#endif

			else
			{
				uint c;
				ctmbstr s = argv[1];

				while ((c = *++s) != '\0')
				{
					switch (c)
					{
					case 'i':
						tidyOptSetInt(tdoc, TidyIndentContent, TidyAutoState);
						if (tidyOptGetInt(tdoc, TidyIndentSpaces) == 0)
							tidyOptResetToDefault(tdoc, TidyIndentSpaces);
						break;

						/* Usurp -o for output file.  Anyone hiding end tags?
						case 'o':
						tidyOptSetBool( tdoc, TidyHideEndTags, yes );
						break;
						*/

					case 'u':
						tidyOptSetBool(tdoc, TidyUpperCaseTags, yes);
						break;

					case 'c':
						tidyOptSetBool(tdoc, TidyMakeClean, yes);
						break;

					case 'g':
						tidyOptSetBool(tdoc, TidyGDocClean, yes);
						break;

					case 'b':
						tidyOptSetBool(tdoc, TidyMakeBare, yes);
						break;

					case 'n':
						tidyOptSetBool(tdoc, TidyNumEntities, yes);
						break;

					case 'm':
						tidyOptSetBool(tdoc, TidyWriteBack, yes);
						break;

					case 'e':
						tidyOptSetBool(tdoc, TidyShowMarkup, no);
						break;

					case 'q':
						tidyOptSetBool(tdoc, TidyQuiet, yes);
						break;

					default:
						unknownOption(c);
						break;
					}
				}
			}

			--argc;
			++argv;
			continue;
		}

		if (argc > 1)
		{
			htmlfil = argv[1];
#if (!defined(NDEBUG) && defined(_MSC_VER))
			SPRTF("Tidying '%s'\n", htmlfil);
#endif // DEBUG outout
			if (tidyOptGetBool(tdoc, TidyEmacs))
				tidyOptSetValue(tdoc, TidyEmacsFile, htmlfil);
			status = tidyParseFile(tdoc, htmlfil);
		}
		else
		{
			htmlfil = "stdin";
			status = tidyParseStdin(tdoc);
		}

		if (status >= 0)
			status = tidyCleanAndRepair(tdoc);

		if (status >= 0) {
			status = tidyRunDiagnostics(tdoc);
			if (!tidyOptGetBool(tdoc, TidyQuiet)) {
				/* NOT quiet, show DOCTYPE, if not already shown */
				if (!tidyOptGetBool(tdoc, TidyShowInfo)) {
					tidyOptSetBool(tdoc, TidyShowInfo, yes);
					tidyReportDoctype(tdoc);  /* FIX20140913: like warnings, errors, ALWAYS report DOCTYPE */
					tidyOptSetBool(tdoc, TidyShowInfo, no);
				}
			}

		}
		if (status > 1) /* If errors, do we want to force output? */
			status = (tidyOptGetBool(tdoc, TidyForceOutput) ? status : -1);

		if (status >= 0 && tidyOptGetBool(tdoc, TidyShowMarkup))
		{
			if (tidyOptGetBool(tdoc, TidyWriteBack) && argc > 1)
				status = tidySaveFile(tdoc, htmlfil);
			else
			{
				ctmbstr outfil = tidyOptGetValue(tdoc, TidyOutFile);
				if (outfil) {
					status = tidySaveFile(tdoc, outfil);
				}
				else {
#if !defined(NDEBUG) && defined(_MSC_VER)
					static char tmp_buf[264];
					sprintf(tmp_buf, "%s.html", get_log_file());
					status = tidySaveFile(tdoc, tmp_buf);
					SPRTF("Saved tidied content to '%s'\n", tmp_buf);
#else
					status = tidySaveStdout(tdoc);
#endif
				}
			}
		}

		contentErrors += tidyErrorCount(tdoc);
		contentWarnings += tidyWarningCount(tdoc);
		accessWarnings += tidyAccessWarningCount(tdoc);

		--argc;
		++argv;

		if (argc <= 1)
			break;
	}

	if (!tidyOptGetBool(tdoc, TidyQuiet) &&
		errout == stderr && !contentErrors)
		fprintf(errout, "\n");

	if (contentErrors + contentWarnings > 0 &&
		!tidyOptGetBool(tdoc, TidyQuiet))
		tidyErrorSummary(tdoc);

	if (!tidyOptGetBool(tdoc, TidyQuiet))
		tidyGeneralInfo(tdoc);

	/* called to free hash tables etc. */
	tidyRelease(tdoc);

	/* return status can be used by scripts */
	if (contentErrors > 0)
		return 2;

	if (contentWarnings > 0)
		return 1;

	/* 0 signifies all is ok */
	return 0;
}

/*
* local variables:
* mode: c
* indent-tabs-mode: nil
* c-basic-offset: 4
* eval: (c-set-offset 'substatement-open 0)
* end:
*/
